//
// GPSMapEdit
// (c) Konstantin Galichsky (kg@geopainting.com), 2002-2007
//
// OziExplorer MAP (raster maps) parser.
//

# include "StdAfx.h"
# include "Resource.h"
# include "OziMapLoader.h"
# include "ParseHelpers.h"
# include "Map.h"
# include "DDBitmap.h"
# include "DIBitmap.h"
# include "Datum.h"
# include "Load.h"
# include "Log.h"
# include "Status.h"
# include "MapView.h"
# include "MathAlgo.h"
# include "MiniGDI+.h"
# include "Ozf2Decoder.h"
# include "fast_floor.h"
# include "fast_log2.h"
# include "Globals.h"

bool CheckImageSize (long _lWidth, long _lHeight) {
	const LONGLONG llMemSize = LONGLONG (_lWidth)*_lHeight*3*2;
	if (llMemSize > 2100000000) {
		ReportWarning ("The image %dx%d size is >330 Mpixels. Required image buffer does not fit the memory limitations applied to 32-bit Windows applications.)", _lWidth, _lHeight);
		return false;
	}

	return true;
}

void ReportOutOfVideoMemory (HDC _hDC, long _lWidth, long _lHeight) {
	const float fMegapixels = _lWidth*_lHeight/(1000.f*1000);
	ReportText ("***********************************************************************");
	ReportText ("*");
	ReportText ("* Image size is %0.2f Mpixels (%dx%d).", fMegapixels, _lWidth, _lHeight);
	ReportText ("* This is too large for EVALUATION mode. Please register GPSMapEdit.");
	ReportText ("*");
	ReportText ("***********************************************************************");

	::SendMessage (g_hWnd, WM_COMMAND, ID_REGISTER_ONLINE, 0);
}

enum LoadImageResult_t {lirOK, lirRetry, lirFail};

////////////////////////////////////////////////////////////

static
BOOL CALLBACK _DrawImageAbortProc (VOID *) {return g_bStop ? TRUE : FALSE;}

static
LoadImageResult_t _PrepareLoadImageByGdiPlus (const char * _strFileName, CGdipImage & _image) {
	//
	// Load GDI+.
	//
	if (! InitGdiPlus ())
		return lirRetry;

	//
	// Convert file name to Unicode string accepted by GDI+.
	//
	WCHAR wstrFileName [_MAX_PATH + 1];
	::MultiByteToWideChar (::GetACP (), 0, _strFileName, -1, wstrFileName, _MAX_PATH);

	//
	// Check image file type.
	//
	WCHAR * wstrFileExt = ::wcsrchr (wstrFileName, L'.');
	if (wstrFileExt == NULL) {
		ReportWarning ("Map image type is not supported.");
		return lirRetry;
	}
	++ wstrFileExt;
	const size_t cFileExtLen = ::wcslen (wstrFileExt);
	::CharUpperW (wstrFileExt);

	bool bExtIsSupported = false;

	// Get list of codecs.
	UINT cNumDecoders = 0, cSizeDecoders = 0;
	g_pfGdipGetImageDecodersSize (& cNumDecoders, & cSizeDecoders);
	ImageCodecInfo * const pImageCodecInfo = reinterpret_cast<ImageCodecInfo *> (alloca (cSizeDecoders));
	g_pfGdipGetImageDecoders (cNumDecoders, cSizeDecoders, pImageCodecInfo);
	for (size_t cCodec = 0; cCodec < cNumDecoders; ++ cCodec) {
		// Enumerate extensions.
		const WCHAR * wstrExtensions = pImageCodecInfo [cCodec].FilenameExtension;

		WCHAR * const wstrExtensions2 = reinterpret_cast<WCHAR * > (alloca ((::wcslen (wstrExtensions) + 1)*sizeof (WCHAR)));
		::wcscpy (wstrExtensions2, wstrExtensions);
		::CharUpperW (wstrExtensions2);

		const WCHAR * pExtEnd = wstrExtensions2;
		while (* pExtEnd != L'\0') {
			const WCHAR * wstrExt = pExtEnd;
			while (* pExtEnd != L';' && * pExtEnd != L'\0')
				++ pExtEnd;

			while (* wstrExt == L'*' || * wstrExt == L'.')
				++ wstrExt;

			const size_t cExtLen = pExtEnd - wstrExt;
			if (cExtLen == cFileExtLen && ::wcsncmp (wstrFileExt, wstrExt, cExtLen) == 0) {
				bExtIsSupported = true;
				break;
			}

			if (* pExtEnd == L';')
				++ pExtEnd;
		}
	}

	if (! bExtIsSupported) {
		ReportWarning ("Image type '.%S' is not supported by GDI+.", wstrFileExt);
		return lirRetry;
	}

	//
	// Load image from file.
	//
	const GpStatus s = _image.LoadImageFromFile (wstrFileName);
	if (s != 0) {
		if (s == UnknownImageFormat)
			// NOTE: GDI+ unexpectedly returns OutOfMemory code for OZF2.
			ReportWarning ("Unknown image format.");
		else {
			if (s == OutOfMemory)
				ReportWarning ("GDI+ failed to load image due to not enough memory.");
			else
				ReportWarning ("GDI+ failed to load image. Error code: %d.", s);
		}

		return lirRetry;
	}

	return lirOK;
}

static
LoadImageResult_t _LoadImageByGdiPlus (const char * _strFileName, HDC _hWndDC, CDDBitmap ** _ppBmp) {
	CGdipImage image;
	const LoadImageResult_t lirPrepare = _PrepareLoadImageByGdiPlus (_strFileName, image);
	if (lirPrepare != lirOK)
		return lirPrepare;

	// Get image's dimensions in pixels.
	const int nWidth  = image.Width ();
	const int nHeight = image.Height ();
	if (! CheckImageSize (nWidth, nHeight))
		return lirFail;

	// Create temporary bitmap used as source buffer.
	CDDBitmap * const pDdb = new CDDBitmap (nWidth, nHeight);
	CObjectHolder<CDDBitmap> pDdb_Holder (pDdb);
	if (pDdb->GetBitmap () == NULL) {
		ReportWarning ("Not enough video memory to render image.");
		ReportOutOfVideoMemory (_hWndDC, nWidth, nHeight);

		return lirFail;
	}

	SetProgressStatus (2);

	// Render the map image to the temporary bitmap.
	CGdipGraphics graphics (pDdb->GetDC ());
	const GpStatus rs = g_pfGdipDrawImageRectRectI (graphics, image, 0, 0, nWidth, nHeight, 0, 0, nWidth, nHeight, UnitPixel, NULL, _DrawImageAbortProc, NULL);
	if (rs != Ok) {
		if (rs == OutOfMemory)
			ReportWarning ("GDI+ failed to render image due to not enough memory.");
		else
			ReportWarning ("GDI+ failed to render image (error code=%d).", rs);
		return lirRetry;
	}

	pDdb_Holder.Detach ();
	* _ppBmp = pDdb;

	return lirOK;
}

//////////////////////////////////////////

static
LoadImageResult_t _PrepareLoadImageByLoadPicture (const char * _strFileName, IPicture ** _ppPicture) {
	ReportText ("Trying to load map image using LoadPicture() API...");

	// Check image file type.
	const char * strFileExt = ::strrchr (_strFileName, '.');
	if (strFileExt == NULL) {
		ReportWarning ("Map image type is not supported.");
		return lirRetry;
	}
	++ strFileExt;
	if (
		::stricmp (strFileExt, "bmp") != 0 &&
		::stricmp (strFileExt, "jpg") != 0 &&
		::stricmp (strFileExt, "gif") != 0
	) {
		ReportWarning ("Image type '.%s' is not supported by LoadPicture() API.", strFileExt);
		return lirRetry;
	}

	// Load image file.
	const CFileForRead hFile (_strFileName);
	if (! hFile.IsValid ()) {
		ReportWarning ("Can't open map image file: %s.", _strFileName);
		return lirRetry;
	}

	const DWORD dwSize = ::GetFileSize (hFile, NULL);
	const HGLOBAL hGlobal = ::GlobalAlloc (GMEM_MOVEABLE, dwSize);
	if (hGlobal == NULL) {
		ReportWarning ("Not enough memory: failed to allocate %d bytes.", dwSize);
		return lirRetry;
	}
	void * const pFile = ::GlobalLock (hGlobal);

	DWORD dwRead = 0;
	::ReadFile (hFile, pFile, dwSize, & dwRead, NULL);
	if (dwRead < dwSize) {
		::GlobalUnlock (hGlobal);
		::GlobalFree (hGlobal);

		ReportWarning ("Can't read map image file: %s.", _strFileName);
		return lirRetry;
	}

	CCom com;

	CComPtr<IStream> pStream;
	::CreateStreamOnHGlobal (hGlobal, TRUE, & pStream);

	HRESULT hr = ::OleLoadPicture (pStream, dwSize, FALSE, IID_IPicture, reinterpret_cast<void **> (_ppPicture));
	if (FAILED (hr)) {
		switch (hr) {
			case E_OUTOFMEMORY:
				ReportWarning ("Failed to read map image: '%s' due to not enough memory.", _strFileName);
				break;
			default:
				ReportWarning ("Failed to read map image: '%s'. Error code: 0x%08x.", _strFileName, hr);
				break;
		}
		return lirRetry;
	}

	return lirOK;
}

static
LoadImageResult_t _LoadImageByLoadPicture (const char * _strFileName, HDC _hWndDC, CDDBitmap ** _ppBmp) {
	CComPtr<IPicture> pPicture;
	const LoadImageResult_t lirPrepare = _PrepareLoadImageByLoadPicture (_strFileName, & pPicture);
	if (lirPrepare != lirOK)
		return lirPrepare;

	// Get image dimensions.
	OLE_XSIZE_HIMETRIC hmWidth;
	pPicture->get_Width (& hmWidth);
	const int nWidth  = ::MulDiv (hmWidth  + 1, ::GetDeviceCaps (_hWndDC, LOGPIXELSX), 2540);

	OLE_YSIZE_HIMETRIC hmHeight;
	pPicture->get_Height (& hmHeight);
	const int nHeight = ::MulDiv (hmHeight + 1, ::GetDeviceCaps (_hWndDC, LOGPIXELSY), 2540);

	if (! CheckImageSize (nWidth, nHeight))
		return lirFail;

	SetProgressStatus (2);

	// Create temporary bitmap used as source buffer.
	CDDBitmap * const pDdb = new CDDBitmap (nWidth, nHeight);
	CObjectHolder<CDDBitmap> pDdb_Holder (pDdb);
	if (pDdb->GetBitmap () == NULL) {
		ReportWarning ("Not enough video memory to render image.");
		ReportOutOfVideoMemory (_hWndDC, nWidth, nHeight);

		return lirFail;
	}

	// Render the map image to the temporary bitmap.
	const HRESULT hr = pPicture->Render (pDdb->GetDC (), 0, nHeight, nWidth, -nHeight, 0, 0, hmWidth, hmHeight, NULL);
	if (FAILED (hr)) {
		ReportWarning ("Failed to render map image: '%s'. Error code: 0x%08x.", _strFileName, hr);
		return lirRetry;
	}

	pDdb_Holder.Detach ();	
	* _ppBmp = pDdb;

	return lirOK;
}

//////////////////////////////////////////

class COziMapLoader : public CMapLoader {
  public:
	// Override.
	virtual bool IsValid (const char * _pData, size_t _cFileSize) const;
	virtual LoadStatus_t Load (const char * _pData, size_t _cFileSize, const char * _strFileName, map_t * _pMap);

  private:
	LoadStatus_t Load2 (const char * _pData, size_t _cFileSize, const char * _strFileName, map_t * _pMap);

	void ParseImageFileName (const char * _p, const char * _pNextLine);
	void ParseDatum         (const char * _p, const char * _pNextLine);
	void ParseProjection    (const char * _p, const char * _pNextLine);
	bool ParseMMPNUM        (const char * _p, const char * _pNextLine);
	void ParseMMPXY         (const char * _p, const char * _pNextLine);
	void ParseMMPLL         (const char * _p, const char * _pNextLine);

	map_t * m_pMap;
	const char * m_pStart, * m_pEnd;

	// The map image file name.
	char m_strImageName [MAX_PATH + 1];

	bool CheckImageFile (const char * _strMapFileName, bool & _bOzf2);

	// Projection type.
	enum {
		ptLL, ptMercator, ptTransverseMercator, ptLambertConic, ptAlbers,
		ptUnknown
	} m_Projection;
	string_t m_strProjection;

	// The quadraic approximation of the projection.
	float m_Axx, m_Axy, m_Bx, m_Dxxy, m_Dxyy;
	float m_Ayx, m_Ayy, m_By, m_Dyxx, m_Dyyy;

	bool PrepareProjection ();

	// The datum.
	const CTransformationBase * m_pDatumTransformation;

	// Corner points.
	point_t m_points [4];
	float   m_lScreenX [4];
	float   m_lScreenY [4];
	// Bitmask of defined corners.
	DWORD   m_dwPointDefined;

	// The bounding rectangle.
	rect_t  m_rectBound;

	bool m_bMMPXY_Defined;
	bool m_bMMPLL_Defined;
	bool PrepareData (bool _bCheck);

	LoadStatus_t RectifyImage (
		const BYTE * _pBits1, size_t _cScanWidth1, size_t _cWidthInPixels,  size_t _cHeightInPixels,
		      BYTE * _pBits2, size_t _cScanWidth2, size_t _cWidthInPixels2, size_t _cHeightInPixels2,
		size_t _cProgressStart, size_t _cProgressEnd
	);
};

DECLARE_MAP_LOADER (COziMapLoader, "ozi-map", "map", "OziExplorer map (*.map)");

bool COziMapLoader::IsValid (const char * _pData, size_t _cFileSize) const {
	if (! CheckAnsiFile (_pData, _cFileSize, true))
		return false;

	return
		// For .map from OziExplorer
		HasPrefix (_pData, _pData + _cFileSize, "OziExplorer Map Data File", 25) ||
		// For .map from ExpertGPS
		HasPrefix (_pData, _pData + _cFileSize, "User Map Calibration File Version", 33);
}

void COziMapLoader::ParseImageFileName (const char * _p, const char * _pNextLine) {
	// Get map image file name.
	const size_t cLen = GetLineLen (_p, _pNextLine);
	char * const pLine = reinterpret_cast<char *> (::alloca (cLen + 1));
	::memcpy (pLine, _p, cLen);
	pLine [cLen] = '\0';

	::memcpy (m_strImageName, pLine, cLen < MAX_PATH ? cLen + 1 : MAX_PATH + 1);
}

void COziMapLoader::ParseDatum (const char * _p, const char * _pNextLine) {
	const char * strDatumEnd = _p;
	while (strDatumEnd < _pNextLine) {
		if (* strDatumEnd == ',' || * strDatumEnd == '\r' || * strDatumEnd == '\n')
			break;
		++ strDatumEnd;
	}
	const string_t strDatum (_p, strDatumEnd);

	if (! FindOziDatumToWGS84 (strDatum.c_str (), m_pDatumTransformation)) {
		m_pDatumTransformation = NULL;
		ReportWarning ("Some unsupported datum is used: '%s'. No datum conversion performed.", strDatum.c_str ());
	}
}

void COziMapLoader::ParseProjection (const char * _p, const char * _pNextLine) {
	const char * const pProjection = _p + 15;
	assert (pProjection < _pNextLine);

	const char * const pComma = reinterpret_cast<const char *> (::memchr (pProjection, ',', _pNextLine - pProjection));
	if (pComma)
		m_strProjection.assign (pProjection, pComma);
	else
		m_strProjection.assign (pProjection, GetLineLen (pProjection, _pNextLine));

	if (m_strProjection == "Latitude/Longitude")
		m_Projection = ptLL;
	else if (
		m_strProjection == "Transverse Mercator" ||
		m_strProjection == "(UTM) Universal Transverse Mercator" ||
		m_strProjection == "(SG) Swedish Grid" ||
		m_strProjection == "(BNG) British National Grid" ||
		m_strProjection == "(IG) Irish Grid" ||
		m_strProjection == "(SUI) Swiss Grid" ||
		m_strProjection == "(ITA1) Italy Grid Zone 1" ||
		m_strProjection == "(ITA2) Italy Grid Zone 2"
	)
		m_Projection = ptTransverseMercator;
	else if (m_strProjection == "Mercator")
		m_Projection = ptMercator;
	else if (m_strProjection == "Lambert Conformal Conic")
		m_Projection = ptLambertConic;
	else if (m_strProjection == "Albers Equal Area")
		m_Projection = ptAlbers;
	else
// "(NZG) New Zealand Grid"
// "(I) France Zone I"
// "(II) France Zone II"
// "(III) France Zone III"
// "(IV) France Zone IV"
// "(A)Lambert Azimuthual Equal Area"
// "Sinusoidal"
// "Polyconic (American)"
// "Van Der Grinten"
// "Vertical Near-Sided Perspective"
// "(WIV) Wagner IV"
// "Bonne"
// "(MT0) Montana State Plane Zone 2500"
		m_Projection = ptUnknown;
}

bool COziMapLoader::ParseMMPNUM (const char * _p, const char * _pNextLine) {
	const int i = ParseDecU (_p + 7, _pNextLine);
	if (i != 4) {
		ReportWarning (_p - m_pStart, "Unexpected value of MMPNUM. Only 4 is supported. Please manually remove all MMPNUM, MMPXY and MMPLL lines, then re-save this .MAP in OziExplorer.");
		return false;
	}
	return true;
}

void COziMapLoader::ParseMMPXY (const char * _p, const char * _pNextLine) {
	m_bMMPXY_Defined = true;

	const char * p = _p + 5;

	OmitSpaces (& p, _pNextLine);
	if (* p == ',')
		++ p;
	OmitSpaces (& p, _pNextLine);

	unsigned long ul = 0;
	if (ParseDecU (p, _pNextLine, & p, ul)) {
		OmitSpaces (& p, _pNextLine);
		if (* p == ',')
			++ p;
		OmitSpaces (& p, _pNextLine);

		unsigned long x = 0;
		if (ParseDecU (p, _pNextLine, & p, x)) {
			OmitSpaces (& p, _pNextLine);
			if (* p == ',')
				++ p;
			OmitSpaces (& p, _pNextLine);

			unsigned long y = 0;
			if (ParseDecU (p, _pNextLine, & p, y)) {
				if (ul <= 4) {
					m_lScreenX [ul - 1] = x;
					m_lScreenY [ul - 1] = y;
					m_dwPointDefined |= 1 << (ul - 1);
				}

				return;
			}
		}
	}
				
	ReportWarning (_p - m_pStart, "Unexpected format of MMPXY line.");
}

void COziMapLoader::ParseMMPLL (const char * _p, const char * _pNextLine) {
	m_bMMPLL_Defined = true;

	const char * p = _p + 5;

	OmitSpaces (& p, _pNextLine);
	if (* p == ',')
		++ p;
	OmitSpaces (& p, _pNextLine);

	unsigned long ul = 0;
	if (ParseDecU (p, _pNextLine, & p, ul)) {
		OmitSpaces (& p, _pNextLine);
		if (* p == ',')
			++ p;
		OmitSpaces (& p, _pNextLine);

		point_t point;
		if (ParseReal (p, _pNextLine, & p, point.x)) {
			OmitSpaces (& p, _pNextLine);
			if (* p == ',')
				++ p;
			OmitSpaces (& p, _pNextLine);

			if (ParseReal (p, _pNextLine, & p, point.y)) {
				if (ul <= 4)
					m_points [ul - 1] = point;
				return;
			}
		}
	}
	ReportWarning (_p - m_pStart, "Unexpected format of MMPLL line.");
}

bool COziMapLoader::PrepareData (bool _bCheck) {
	// Check if all required fields are defined.
	if (m_dwPointDefined == 0) {
		if (_bCheck && m_bMMPXY_Defined && m_bMMPLL_Defined) {
			ReportWarning ("The activation key is probably damaged.");
		} else {
			ReportWarning ("No corner points defined (expected in MMPXY and MMPLL lines).");
			ReportText ("Please try to update the .MAP file with newer version of OziExplorer.");
		}
		return false;
	} else if (m_dwPointDefined != 0xF) {
		ReportWarning ("Not enough corner points (4 expected in MMPXY and MMPLL).");
		return false;
	}

	// Convert datum.
	if (m_pDatumTransformation) {
		ReportText ("Performing datum conversion to WGS84...");

		for (size_t cPoint = 0; cPoint < 4; ++ cPoint)
			m_pDatumTransformation->ApplyTo (m_points [cPoint]);
	}

	// Estimate the bounding rectangle.
	for (size_t cPoint = 0; cPoint < 4; ++ cPoint)
		m_rectBound.Extend (m_points [cPoint]);

	return true;
}

bool COziMapLoader::PrepareProjection () {
	ReportText ("Performing projection calculations...");

	m_Dxxy = 0;
	m_Dxyy = 0;
	m_Dyxx = 0;
	m_Dyyy = 0;
	switch (m_Projection) {
		case ptLL:
			if (
				! Solve3x3 (m_points, m_lScreenX, m_Axx, m_Axy, m_Bx) ||
				! Solve3x3 (m_points, m_lScreenY, m_Ayx, m_Ayy, m_By)
			) {
				ReportWarning ("Some calibration points belong to a line. Please check the map calibration.");
				return false;
			}
			break;

		case ptTransverseMercator: {
			if (
				! Solve4x4_XY (m_points, m_lScreenX, m_Axx, m_Axy, m_Bx, m_Dxxy) ||
//				! Solve4x4_XX (m_points, m_lScreenY, m_Ayx, m_Ayy, m_By, m_Dyxx)
				! Solve3x3    (m_points, m_lScreenY, m_Ayx, m_Ayy, m_By)
			) {
				ReportWarning ("Some calibration points belong to a line. Please check the map calibration.");
				return false;
			}

			// Calculate non-linear factor (quadratic approximation).
			const float t = .25*::sin (c_PI/180*m_rectBound.MeanY ())*c_PI/180;
			m_Dyxx = m_Ayy*t;

			// Make corrections to apply the non-linear factor.
			const float x0 = m_rectBound.x0;
			const float x1 = m_rectBound.x1;
			m_Ayx -= m_Dyxx*(x0 + x1);
			m_By += m_Dyxx*x0*x1;

			break;
		}

		case ptMercator: {
			// Find parameters of linear transform.
			if (
				! Solve3x3 (m_points, m_lScreenX, m_Axx, m_Axy, m_Bx) ||
				! Solve3x3 (m_points, m_lScreenY, m_Ayx, m_Ayy, m_By)
			) {
				ReportWarning ("Some calibration points belong to a line. Please check the map calibration.");
				return false;
			}

			// Calculate non-linear factor (quadratic approximation).
			const float y0 = m_rectBound.y0;
			const float y1 = m_rectBound.y1;
			const float t = .5*::tan (c_PI/180*y0)*c_PI/180;

			// Make corrections to apply the non-linear factor.
			m_Dyyy = m_Ayy*t;
			m_Ayy -= m_Dyyy*(y0 + y1);
			m_By += m_Dyyy*y0*y1;

			// Check if quadratic aproximation of tan() is not enough.
			if (y0*y1 < 0)
				ReportWarning ("Mercator projection transform may have large distorsion for this map.");
			const float fi0 = c_PI/180*y0;
			const float fi1 = c_PI/180*y1;
			const float fTan0 = ::tan (fi0);
			const float fTan1 = ::tan (fi1);
			const float fCos0 = ::cos (fi0);
			const float d = fTan1 - fTan0 - (fi1 - fi0)/(fCos0*fCos0) - fTan0/(fCos0*fCos0)*(fi1 - fi0)*(fi1 - fi0);
			const float iD = d*m_Ayy*(y1 - y0);
			if (::fabs (iD) >= 1)
				ReportWarning ("Mercator projection transform is based on quadratic approximation. Distorsion in center of image may be about %0.1f (in pixels).", iD);

			break;
		}

		case ptAlbers:
		case ptLambertConic: {
			if (
				! Solve4x4_XY (m_points, m_lScreenX, m_Axx, m_Axy, m_Bx, m_Dxxy) ||
				! Solve3x3    (m_points, m_lScreenY, m_Ayx, m_Ayy, m_By)
			) {
				ReportWarning ("Some calibration points belong to a line. Please check the map calibration.");
				return false;
			}

			if (m_Ayy != 0) {
				// Calculate the non-linear factor.
				const float t = ::cos (c_PI/180*m_rectBound.MeanY ());
				m_Dyxx = -.5f*t*t*m_Axx*m_Dxxy/m_Ayy;

				// Make corrections to apply the non-linear factor.
				const float x0 = m_rectBound.x0;
				const float x1 = m_rectBound.x1;
				m_Ayx -= m_Dyxx*(x0 + x1);
				m_By += m_Dyxx*x0*x1;
			}

			break;
		}

		default:
			assert (false);
		case ptUnknown:
			ReportWarning ("The projection '%s' is not supported.", m_strProjection.c_str ());
			return false;
	}

	// Check distorsion of the corner points of the map.
	for (size_t cPoint = 0; cPoint < 4; ++ cPoint) {
		const float fScreenDX = m_lScreenX [cPoint] - (m_Bx + m_Axx*m_points [cPoint].x + m_Axy*m_points [cPoint].y + m_Dxxy*m_points [cPoint].x*m_points [cPoint].y + m_Dxyy*m_points [cPoint].y*m_points [cPoint].y);
		const float fScreenDY = m_lScreenY [cPoint] - (m_By + m_Ayx*m_points [cPoint].x + m_Ayy*m_points [cPoint].y + m_Dyxx*m_points [cPoint].x*m_points [cPoint].x + m_Dyyy*m_points [cPoint].y*m_points [cPoint].y);
		if (fScreenDX*fScreenDX + fScreenDY*fScreenDY >= 1)
			ReportWarning ("Distorsion in image corner #%d is %0.1f, %0.1f (in pixels).", cPoint + 1, fScreenDX, fScreenDY);
	}

	return true;
}

LoadStatus_t COziMapLoader::RectifyImage (
	const BYTE * _pBits1, size_t _cScanWidth1, size_t _cWidthInPixels,  size_t _cHeightInPixels,
	      BYTE * _pBits2, size_t _cScanWidth2, size_t _cWidthInPixels2, size_t _cHeightInPixels2,
	size_t _cProgressStart, size_t _cProgressEnd
) {
	SetProgressStatus (_cProgressStart);
	ReportText ("Performing image rectification...");

	// Inflate the rect to take right and bottom edges into the visible area.
	m_rectBound.x1 += m_rectBound.Width  ()/(_cWidthInPixels2  - 1);
	m_rectBound.y1 += m_rectBound.Height ()/(_cHeightInPixels2 - 1);

	// Perform projection rectification.
	for (size_t y2 = 0; y2 < _cHeightInPixels2; ++ y2) {
		if (g_bStop)
			return lsInterrupted;

		// Latitude, in degrees.
		const float fY2 = m_rectBound.y0 + y2*m_rectBound.Height ()/_cHeightInPixels2;

		const float fKx_ = m_rectBound.Width ()/_cWidthInPixels2;

		const float fAx_ = m_Axx + m_Dxxy*fY2;
		const float fBx_ = m_Bx + m_Axy*fY2 + m_Dxyy*fY2*fY2 + .5f;
		const float fBy_ = m_By + m_Ayy*fY2 + m_Dyyy*fY2*fY2 + .5f;

		for (size_t x2 = 0; x2 < _cWidthInPixels2; ++ x2) {
			// Longitude, in degrees.
			const float fX2 = m_rectBound.x0 + x2*fKx_;

			// Coordinates inside pBits1.
			// Axx*fX2 + Axy*fY2 + Bx + Dxxy*fX2*fY2 + Dxyy*fY2*fY2 + .5f;
			const size_t x1 = fast_floor (fAx_*fX2 + fBx_);
			if (0 > x1 || x1 >= _cWidthInPixels)
				continue;
			// Ayx*fX2 + Ayy*fY2 + By + Dyxx*fX2*fX2 + Dyyy*fY2*fY2 + .5f;
			const size_t y1 = fast_floor ((m_Ayx + m_Dyxx*fX2)*fX2 + fBy_);
			if (0 > y1 || y1 >= _cHeightInPixels)
				continue;

			const BYTE * const pPixel1 = _pBits1 + x1*3 + (_cHeightInPixels - 1 - y1)*_cScanWidth1;
			      BYTE * const pPixel2 = _pBits2 + x2*3 + y2*_cScanWidth2;
			pPixel2 [0] = pPixel1 [0];
			pPixel2 [1] = pPixel1 [1];
			pPixel2 [2] = pPixel1 [2];
		}

		SetProgressStatus (_cProgressStart + fast_floor (float (_cProgressEnd - _cProgressStart)*y2/_cHeightInPixels2 + .5f));
	}

	SetProgressStatus (_cProgressEnd);

	return lsOK;
}

bool COziMapLoader::CheckImageFile (const char * _strMapFileName, bool & _bOzf2) {
	ReportText ("Loading map image '%s'...", m_strImageName);

	//
	// Check extension.
	//
	const char * pFileExt = ::strrchr (m_strImageName, L'.');
	if (pFileExt == NULL) {
		ReportWarning ("Map image type is not supported.");
		return false;
	}
	++ pFileExt;
	const size_t cFileExtLen = ::strlen (pFileExt);

	_bOzf2 = ::stricmp (pFileExt, "ozf2") == 0;

	if (::stricmp (pFileExt, "ecw") == 0) {
		ReportWarning ("Map image type '.ECW' referred from MAP file is not supported. Please open .ECW file directly.");
		return false;
	}

	if (
		::stricmp (pFileExt, "ozfx3") == 0 ||
		::stricmp (pFileExt, "sid")   == 0
	) {
		ReportWarning ("Map image type '.%s' is not supported. Please convert it to BMP, JPG, GIF or TIFF.", pFileExt);
		return false;
	}

	//
	// Check file.
	//
	const CFileForRead hFile (m_strImageName);
	if (hFile.IsValid ())
		return true;

	ReportText ("Failed to open '%s'.", m_strImageName);

	// Make image name local for folder with _strMapFileName.
	char strLocalImageName [MAX_PATH];
	::strcpy (strLocalImageName, _strMapFileName);
	char * strMapPathEnd = ::strrchr (strLocalImageName, '\\');
	if (strMapPathEnd)
		++ strMapPathEnd;
	else 
		strMapPathEnd = strLocalImageName;

	const char * strImageName = ::strrchr (m_strImageName, '\\');
	if (strImageName)
		++ strImageName;
	else
		strImageName = m_strImageName;

	::strcpy (strMapPathEnd, strImageName);

	ReportText ("Trying '%s'...", strLocalImageName);

	// Try the 'local' name.
	const CFileForRead hFileLocal (strLocalImageName);
	if (! hFileLocal.IsValid ()) {
		ReportWarning ("Can't open map image: %s.", m_strImageName);
		return false;
	}

	::strcpy (m_strImageName, strLocalImageName);

	return true;
}

# include "OziMapLoader.inl"

LoadStatus_t COziMapLoader::Load (const char * _pData, size_t _cFileSize, const char * _strMapFileName, map_t * _pMap) {
	m_pMap   = _pMap;
	m_pStart = _pData;
	m_pEnd   = _pData + _cFileSize;

	//
	// Parse .map file.
	//

	m_strImageName [0] = '\0';
	m_rectBound = rect_t ();
	m_Projection = ptUnknown;
	m_pDatumTransformation = NULL;
	m_dwPointDefined = 0;
	m_bMMPXY_Defined = false;
	m_bMMPLL_Defined = false;

# if defined (OZIMAPLOADER_PRIVATE)
	if (g_bRegistered)
		return Load2 (_pData, _cFileSize, _strMapFileName, _pMap);
# endif
	ReportText ("***********************************************************************");
	ReportText ("*");
	ReportText ("* Evaluation mode: huge size images are not supported.");
	ReportText ("*");
	ReportText ("***********************************************************************");

	SetProgressStatus (0);

	size_t cFileLine = 1;
	const char * pLine = _pData;
	do {
		if (g_bStop)
			return lsInterrupted;

		const char * const pNextLine = FindNextLine (pLine, m_pEnd);

		if (pLine [0] != '\n' && pLine [0] != '\r') {
			if (cFileLine == 3)
				ParseImageFileName (pLine, pNextLine);
			else if (cFileLine == 5)
				ParseDatum (pLine, pNextLine);
			else {
				if (HasPrefix (pLine, pNextLine, "Map Projection,", 15))
					ParseProjection (pLine, pNextLine);
				else if (HasPrefix (pLine, pNextLine, "MMPNUM,", 7)) {
					if (! ParseMMPNUM (pLine, pNextLine))
						return lsFailed;
				} else if (HasPrefix (pLine, pNextLine, "MMPXY,", 6))
					ParseMMPXY (pLine, pNextLine);
				else if (HasPrefix (pLine, pNextLine, "MMPLL,", 6))
					ParseMMPLL (pLine, pNextLine);
			}
		}

		++ cFileLine;

		pLine = pNextLine;
	} while (pLine < _pData + _cFileSize);

	if (! PrepareData (false))
		return lsFailed;
	if (! PrepareProjection ())
		return lsFailed;

	SetProgressStatus (1);

	//
	// Load map image file.
	//

	bool bOzf2 = false;
	if (! CheckImageFile (_strMapFileName, bOzf2))
		return lsFailed;

	const CClientDC hWndDC (g_hMapWnd);

	CDDBitmap * pBmp1 = NULL;
	LoadImageResult_t lir;
	if (bOzf2)
		lir =      LoadOzf2Image          (m_strImageName, hWndDC, & pBmp1) ? lirOK : lirFail;
	else {
		lir =     _LoadImageByGdiPlus     (m_strImageName, hWndDC, & pBmp1);
		if (lir == lirRetry)
			lir = _LoadImageByLoadPicture (m_strImageName, hWndDC, & pBmp1);
	}

	CObjectHolder<CBitmap> pBmp1_Holder (pBmp1);

	if (lir != lirOK) {
		ReportWarning ("Failed to render image file: '%s'.", m_strImageName);
		return lsFailed;
	}
	if (g_bStop)
		return lsInterrupted;

	SetProgressStatus (10);

	size_t cWidthInPixels1  = pBmp1->Width ();
	size_t cHeightInPixels1 = pBmp1->Height ();

	if (cWidthInPixels1 <= 1 || cHeightInPixels1 <= 1) {
		ReportWarning ("Image should have dimensions > 1 pixel.");
		return lsFailed;
	}

	//
	// Transform the image.
	//

	// NOTE: take rotation into account.
	const double fAngle = ::fabs (::atan2 (m_Ayx, m_Axx));
	const double fCos = fAngle > .3 ? ::cos (fAngle) : 1;
	const double fSin = fAngle > .3 ? ::sin (fAngle) : 0;
	const size_t cWidthInPixels2  = ::labs (fast_floor (fCos*cWidthInPixels1  + fSin*cHeightInPixels1 + .5f));
	const size_t cHeightInPixels2 = ::labs (fast_floor (fCos*cHeightInPixels1 + fSin*cWidthInPixels1  + .5f));

	// Create bimap to keep image ready for rendering (i.e. transformed to the destination projection).
	// NOTE: it should be DDB to provide fast rendering using ::StretchBlt().
	CDDBitmap * const pBmp2 = new CDDBitmap (cWidthInPixels2, cHeightInPixels2);
	CCPtr<CBitmap> pBmp2_cp = pBmp2;
	if (pBmp2->GetBitmap () == NULL) {
		ReportWarning ("Not enough video memory to rectify the image.");
		ReportOutOfVideoMemory (hWndDC, cWidthInPixels2, cHeightInPixels2);
		return lsFailed;
	}

	// Access source bits to perform projection's transform.
	BITMAPINFOHEADER bmih;
	::memset (& bmih, 0, sizeof (bmih));
	bmih.biSize        = sizeof (bmih);
	bmih.biWidth       = cWidthInPixels1;
	bmih.biHeight      = cHeightInPixels1;
	bmih.biPlanes      = 1;
	bmih.biBitCount    = 24;
	bmih.biCompression = BI_RGB;

	const size_t cScanWidth1 = Align4 (3*cWidthInPixels1);
	BYTE * const pBits1 = new BYTE [cScanWidth1*cHeightInPixels1];
	CByteArrayHolder pBits1_Holder (pBits1);

	for (size_t cScanLine = 0; cScanLine < cHeightInPixels1; cScanLine += 100) {
		if (g_bStop)
			return lsInterrupted;

		::GetDIBits (
			pBmp1->GetDC (), pBmp1->GetBitmap (),
			cScanLine, cHeightInPixels1 - cScanLine > 100 ? 100 : cHeightInPixels1 - cScanLine,
			pBits1 + cScanLine*cScanWidth1, reinterpret_cast<BITMAPINFO *> (& bmih), DIB_RGB_COLORS
		);

		SetProgressStatus (fast_floor (10 + 10.f*cScanLine/cHeightInPixels1 + .5f));
	}

	// Prepare destination bits.
	const size_t cScanWidth2 = Align4 (3*cWidthInPixels2);
	BYTE * const pBits2 = new BYTE [cScanWidth2*cHeightInPixels2];
	CByteArrayHolder pBits2_Holder (pBits2);
	::memset (pBits2, 255, cScanWidth2*cHeightInPixels2);

	SetStatus (GetStr (IDS_STATUS_RECTIFYING));

	const LoadStatus_t lsRectify = RectifyImage (
		pBits1, cScanWidth1, cWidthInPixels1, cHeightInPixels1,
	    pBits2, cScanWidth2, cWidthInPixels2, cHeightInPixels2,
		20, 95
	);
	if (lsRectify != lsOK)
		return lsRectify;

	bmih.biWidth  = cWidthInPixels2;
	bmih.biHeight = cHeightInPixels2;
	::SetDIBits (pBmp2->GetDC (), pBmp2->GetBitmap (), 0, cHeightInPixels2, pBits2, reinterpret_cast<BITMAPINFO *> (& bmih), DIB_RGB_COLORS);

	SetProgressStatus (97);

	// Shift the visible rectangle to make binding to the center of points.
	m_rectBound.x0 -= .5f*m_rectBound.Width  ()/(cWidthInPixels2 - 1);
	m_rectBound.x1 -= .5f*m_rectBound.Width  ()/(cWidthInPixels2 - 1);
	m_rectBound.y0 -= .5f*m_rectBound.Height ()/(cHeightInPixels2 - 1);
	m_rectBound.y1 -= .5f*m_rectBound.Height ()/(cHeightInPixels2 - 1);

	// Append the resulting image to the map.
	image_t * const pImage = new image_t;
	pImage->strFileName = _strMapFileName;
	pImage->rect        = m_rectBound;
	pImage->pBitmap     = pBmp2_cp;
	pImage->MakeSmall ();
	m_pMap->renderers.push_back (pImage);

	// Suppress warning about empty Zoom etc.
	m_pMap->bNoHeader = true;

	m_pMap->cHeaderLen = _cFileSize;

	SetProgressStatus (100);

	return lsOK;
}
